# breeder.rb
# Copyright (c) 2009 Alex Wilson
# Licensed under the MIT license (see LICENSE in the distribution root dir)

require "timetable"
require "rubygems"
require "gga4r"

module Timetable
module Genetics

	# An individual for breeding in the genetic simulator.
	class Individual < Array
		@@fit_cache = Hash.new

		def initialize(series, *k)
			super(*k)
			@series = series
		end

		def stats
			[]
		end

		# Induces a random mutation in this individual		
		def mutate
			mutate_point = Kernel.rand(size)
			series = @series[mutate_point]
			vals = series.groups.keys
			self[mutate_point] = vals[Kernel.rand(vals.size)]
		end

		# Recombines (breeds) with the other individual, returns the offspring
		# as an Array of Individuals
		def recombine(c2)
			cross_point = Kernel.rand(size)
			c1_a, c1_b = self.separate(cross_point)
			c2_a, c2_b = c2.separate(cross_point)
			[Individual.new(@series, c1_a + c2_b),
			Individual.new(@series, c2_a + c1_b)]
		end
		
		# Gets the Timetable groups associated with this individual
		def groups
			(0..self.size-1).collect do |i|
				@series[i].groups[self[i]]
			end
		end
		
		# Calculates the fitness function for this individual
		def fitness
			return @@fit_cache[self] if @@fit_cache[self]
			
			grps = groups
			
			score = 1.0
			
			(0..self.size-1).each do |i|
				g = grps[i]
				(i+1..self.size-1).each do |ix|
					g2 = grps[ix]
					score -= 0.4 * g.clash_weight(g2)
				end
			end
			
			sessions = Array.new
			grps.each { |g| sessions += g.sessions }
			
			sessions.each do |s|
				til,s2 = s.next_of(sessions)
				if s2
					case
						when til < 3600
							score -= 0.04
						when til > 3600*2
							score -= 0.02
						when til > 3600*3
							score -= 0.12
					end
				end
			end
			
			score = 0.0 if score < 0.0
			@@fit_cache[self] = score
		end
	end
	
	# The Breeder factory class, for performing genetic simulations
	class Breeder
		attr_reader :ga
	
		def initialize(series, opts = {})
			@series = series
			@options = opts
			@options[:generations] = 10 if not @options[:generations]
			@options[:initial_population] = 20 if not @options[:initial_population]
		end
		
		def create_pop
			pop = []
			#puts "creating population..."
			@options[:initial_population].times do
				indiv = Individual.new( @series, @series.collect do |s|
					k = s.groups.keys
					k[rand(k.size)]
				end )
				pop << indiv
			end
			pop
		end
		
		def run
			@ga = GeneticAlgorithm.new(create_pop, @options)
			@options[:generations].times do |i|
				#puts "running gen #{i}"
				@ga.evolve
				yield i+1, @options[:generations] if block_given?
				#puts "FINDME: generation #{i} done"
			end
		end
		
		def best
			@ga.best_fit[0]
		end
	end

end
end
